# Моделирование ООП

### Введение

Scala предоставляет все необходимые инструменты для объектно-ориентированного проектирования:

- **Traits** позволяют указывать (абстрактные) интерфейсы, а также конкретные реализации.
- **Mixin Composition** предоставляет инструменты для создания компонентов из более мелких деталей.
- **Классы** могут реализовывать интерфейсы, заданные `traits`.
- **Экземпляры** классов могут иметь свое собственное приватное состояние.
- **Subtyping** позволяет использовать экземпляр одного класса там, где ожидается экземпляр его суперкласса.
- **Модификаторы доступа** позволяют управлять, к каким членам класса можно получить доступ с помощью какой части кода.


### Traits

Возможно, в отличие от других языков программирования с поддержкой ООП таких, как Java, 
основным инструментом декомпозиции в Scala являются не классы, а `traits`. 
Они могут служить для описания абстрактных интерфейсов, таких как:

```dotty
trait Showable:
  def show: String
```

, а также могут содержать конкретные реализации:

```dotty
trait Showable:
  def show: String
  def showHtml = "<p>" + show + "</p>"
```

На примере видно, что метод `showHtml` определяется в терминах абстрактного метода `show`.

[Odersky и Zenger][Odersky and Zenger] 
представляют сервис-ориентированную компонентную модель и рассматривают:

- _абстрактные члены_ как требуемые службы: их все еще необходимо реализовать в подклассе. 
- _конкретные члены_ как предоставляемые услуги: они предоставляются подклассу.

Это видно на примере со `Showable`: определяя класс `Document`, который расширяет `Showable`, 
все еще нужно определить `show`, но `showHtml` уже предоставляется:

```dotty
class Document(text: String) extends Showable:
  def show = text
```

#### Абстрактные методы

Абстрактными в `trait` могут оставаться не только методы. `trait` может содержать:

- абстрактные методы (`def m(): T`)
- абстрактные переменные (`val x: T`)
- абстрактные типы (`type T`), потенциально с ограничениями (`type T <: S`)
- абстрактные given (`given t: T`) - подробнее об `given` в следующих главах

Каждая из вышеперечисленных функций может быть использована для определения той или иной формы требований к реализатору `trait`.


### Mixin Composition

Кроме того, что `trait`-ы могут содержать абстрактные и конкретные определения, 
Scala также предоставляет мощный способ создания нескольких `trait`: 
структура, которую часто называют смешанной композицией.

Предположим, что следующие два (потенциально независимо определенные) `trait`-а:

```dotty
trait GreetingService:
  def translate(text: String): String
  def sayHello = translate("Hello")
  
trait TranslationService:
  def translate(text: String): String = "..."
```

Чтобы скомпоновать два сервиса, можно просто создать новый `trait`, расширяющий их:

```dotty
trait ComposedService extends GreetingService, TranslationService
```

Абстрактные элементы в одном `trait`-е (например, `translate` в `GreetingService`) 
автоматически сопоставляются с конкретными элементами в другом `trait`-е. 
Это работает не только с методами, как в этом примере, 
но и со всеми другими абстрактными членами, упомянутыми выше (то есть типами, переменными и т.д.).


### Классы

`trait`-ы отлично подходят для модуляции компонентов и описания интерфейсов (обязательных и предоставляемых). 
Но в какой-то момент возникнет необходимость создавать их экземпляры. 
При разработке программного обеспечения в Scala часто бывает полезно рассмотреть возможность использования классов 
только на начальных этапах модели наследования:

Traits - `T1`, `T2`, `T3`

Composed traits - `S extends T1, T2`, `S extends T2, T3`

Classes - `C extends S, T3`

Instances - `C()`

Это еще более актуально в Scala 3, где `trait`-ы теперь также могут принимать параметры конструктора, 
что еще больше устраняет необходимость в классах.

#### Определение класса

Подобно `trait`-ам, классы могут расширять несколько `trait`-ов (но только один суперкласс):

```dotty
class MyService(name: String) extends ComposedService, Showable:
  def show = s"$name says $sayHello"
```

#### Subtyping

Экземпляр `MyService` создается следующим образом:

```dotty
val s1: MyService = MyService("Service 1")
```

С помощью подтипов экземпляр `s1` можно использовать везде, где ожидается любое из расширенных свойств:

```dotty
val s2: GreetingService = s1
val s3: TranslationService = s1
val s4: Showable = s1
// ... и т.п. ...
```

#### Планирование расширения

Как упоминалось ранее, можно расширить еще один класс:

```dotty
class Person(name: String)
class SoftwareDeveloper(name: String, favoriteLang: String)
  extends Person(name)
```

Однако, поскольку `trait`-ы разработаны как основное средство декомпозиции, 
класс, определенный в одном файле, не может быть расширен в другом файле. 
Чтобы разрешить это, базовый класс должен быть помечен как открытый:

```dotty
open class Person(name: String)
```

Маркировка классов с помощью `open` - это новая функция Scala 3. 
Необходимость явно помечать классы как открытые позволяет избежать многих распространенных ошибок в ООП. 
В частности, это требует, чтобы разработчики библиотек явно планировали расширение 
и, например, документировали классы, помеченные как открытые.

Пример:

```dotty
// File Writer.scala
package p

open class Writer[T]:

  /** Sends to stdout, can be overridden */
  def send(x: T) = println(x)

  /** Sends all arguments using `send` */
  def sendAll(xs: T*) = xs.foreach(send)
end Writer

// File EncryptedWriter.scala
package p

class EncryptedWriter[T: Encryptable] extends Writer[T]:
  override def send(x: T) = super.send(encrypt(x))
```

Открытый класс обычно поставляется с некоторой документацией, 
описывающей внутренние шаблоны вызовов между методами класса, а также хуки, которые можно переопределить. 
Это называется контрактом расширения класса (_extension contract_). 
Он отличается от внешнего контракта (_external contract_) между классом и его пользователями.

Классы без модификатора `open` все же могут быть расширены, 
но только при соблюдении хотя бы одного из двух альтернативных условий:

- Расширяющий класс находится в том же исходном файле, что и расширенный класс.
В этом случае расширение обычно является внутренним вопросом реализации.
- Для класса расширения включена языковая функция
[adhocExtensions](https://scala-lang.org/api/3.x/scala/runtime/stdLibPatches/language$$adhocExtensions$.html). 
Обычно она включается предложением импорта в исходном файле расширения: `import scala.language.adhocExtensions`
Кроме того, эту функцию можно включить с помощью опции компилятора `-language:adhocExtensions`. 
Если эта функция не включена, компилятор выдаст  "feature" warning.

[Подробности об open классах](https://docs.scala-lang.org/scala3/reference/other-new-features/open-classes.html).


### Экземпляры и приватное изменяемое состояние

Как и в других языках с поддержкой ООП, `trait`-ы и классы в Scala могут определять изменяемые поля:

```dotty
class Counter:
  private var currentCount = 0

  def tick(): Unit = currentCount += 1
  def count: Int = currentCount
```

Каждый экземпляр класса `Counter` имеет собственное приватное состояние, 
которое можно наблюдать только через метод `count`, как показано в следующем примере:

```dotty
val c1 = Counter()
c1.count
// res0: Int = 0
c1.tick()
c1.tick()
c1.count
// res3: Int = 2
```

#### Модификаторы доступа

По умолчанию все определения элементов в Scala являются общедоступными. 
Чтобы скрыть детали реализации, можно определить элементы (методы, поля, типы и т.д.) как `private` или `protected`.
Таким образом можно контролировать, как к ним обращаются или как их переопределяют. 
Закрытые (`private`) члены видны только самому классу/`trait`-у и его сопутствующему объекту. 
Защищенные (`protected`) члены также видны подклассам класса.

### Дополнительный пример: сервис-ориентированный дизайн

Далее будут проиллюстрированы некоторые расширенные возможности Scala и показано, 
как их можно использовать для структурирования более крупных программных компонентов. 
Примеры взяты из статьи Мартина Одерски и Маттиаса Зенгера [Масштабируемые компонентные абстракции][Odersky and Zenger]. 
Пример в первую очередь предназначен для демонстрации того, как использовать несколько функций типа 
для создания более крупных компонентов.

Цель состоит в том, чтобы определить программный компонент с семейством типов, 
которые могут быть уточнены позже при реализации компонента. 
Конкретно, следующий код определяет компонент `SubjectObserver` как `trait` с двумя членами абстрактного типа, 
`S` (для субъектов) и `O` (для наблюдателей):

```dotty
trait SubjectObserver:
  type S <: Subject
  type O <: Observer
  
  trait Subject { self: S =>
    private var observers: List[O] = List()
    def subscribe(obs: O): Unit =
      observers = obs :: observers
    def publish(): Unit =
      for obs <- observers do obs.notify(this)
  }
  
  trait Observer:
    def notify(sub: S): Unit
```

Есть несколько вещей, которые нуждаются в объяснении.

##### Члены абстрактного типа

Тип объявления `S <: Subject` говорит, что внутри `trait` `SubjectObserver` можно ссылаться 
на некоторый неизвестный (то есть абстрактный) тип, который называется `S`. 
Однако этот тип не является полностью неизвестным: мы знаем, по крайней мере, что это какой-то подтип `Subject`. 
Все `trait`-ы и классы, расширяющие `SubjectObserver`, могут свободно выбирать любой тип для `S`, 
если выбранный тип является подтипом `Subject`. 
Часть `<: Subject` декларации также упоминается как верхняя граница на `S`.

##### Вложенные `trait`-ы

В рамках `trait`-а `SubjectObserver` определяются два других `trait`-а. 
`trait` `Observer`, который определяет только один абстрактный метод `notify` с одним аргументом типа `S`. 
Как будет видно, важно, чтобы аргумент имел тип `S`, а не тип `Subject`.

Второй `trait`, `Subject`, определяет одно приватное поле `observers` для хранения всех наблюдателей, 
подписавшихся на этот конкретный объект. Подписка на объект просто сохраняет объект в списке. 
Опять же, тип параметра `obs` - это `O`, а не `Observer`.

##### Аннотации собственного типа

Наконец, что означает `self: S =>` в `trait`-е `Subject`? Это называется аннотацией собственного типа. 
И требует, чтобы подтипы `Subject` также были подтипами `S`. 
Это необходимо, чтобы иметь возможность вызывать `obs.notify` с `this` в качестве аргумента, 
поскольку для этого требуется значение типа `S`. 
Если бы `S` был конкретным типом, аннотацию собственного типа можно было бы заменить на `trait Subject`, 
расширяющий `S`.

#### Реализация компонента 

Теперь можно реализовать вышеуказанный компонент и определить члены абстрактного типа как конкретные типы:

```dotty
object SensorReader extends SubjectObserver:
  type S = Sensor
  type O = Display

  class Sensor(val label: String) extends Subject:
    private var currentValue = 0.0
    def value = currentValue
    def changeValue(v: Double): Unit =
      currentValue = v
      publish()

  class Display extends Observer:
    def notify(sub: Sensor): Unit =
      println(s"${sub.label} has value ${sub.value}")
```

В частности, мы определяем singleton object `SensorReader`, который расширяет `SubjectObserver`. 
В реализации `SensorReader` говорится, что `type` `S` теперь определяется как `type` `Sensor`, 
а `type` `O` определяется как `type` `Display`. 
И `Sensor`, и `Display` определяются как вложенные классы в `SensorReader`, 
реализующие `trait`-ы `Subject` и `Observer` соответственно.

Помимо того, что этот код является примером сервис-ориентированного дизайна, 
он также освещает многие аспекты объектно-ориентированного программирования:

- Класс `Sensor` вводит свое собственное частное состояние (`currentValue`) 
и инкапсулирует изменение состояния за методом `changeValue`.
- Реализация `changeValue` использует метод `publish`, определенный в родительском `trait`-е.
- Класс `Display` расширяет `trait` `Observer` и реализует отсутствующий метод `notify`.

Важно отметить, что реализация `notify` может безопасно получить доступ только к `label` и `value` `sub`, 
поскольку мы изначально объявили параметр типа `S`.

#### Использование компонента

Наконец, следующий код иллюстрирует, как использовать компонент `SensorReader`:

```dotty
import SensorReader.*
// настройка сети
val s1 = Sensor("sensor1")
val s2 = Sensor("sensor2")
val d1 = Display()
val d2 = Display()
s1.subscribe(d1)
s1.subscribe(d2)
s2.subscribe(d1)
// распространение обновлений по сети
s1.changeValue(2)
// sensor1 has value 2.0
// sensor1 has value 2.0
s2.changeValue(3)
// sensor2 has value 3.0
```

Имея под рукой все утилиты объектно-ориентированного программирования, 
в следующем разделе будет продемонстрировано, как разрабатывать программы в функциональном стиле.

---

**Ссылки:**

- [Scala3 book, domain modeling oop](https://docs.scala-lang.org/scala3/book/domain-modeling-oop.html)
- [Scala3 book, taste modeling](https://docs.scala-lang.org/scala3/book/taste-modeling.html)
- [Scala3 book, taste objects](https://docs.scala-lang.org/scala3/book/taste-objects.html)
- [Odersky and Zenger. Scalable component abstractions][Odersky and Zenger]
- [Scala 3 Reference, Open classes](https://docs.scala-lang.org/scala3/reference/other-new-features/open-classes.html)
- [Functional Programming vs OOP](https://scalac.io/blog/functional-programming-vs-oop/)

[Odersky and Zenger]: https://dl.acm.org/doi/10.1145/1103845.1094815
